Skip to content

Conversation

devshgraphicsprogramming
Copy link
Member

@devshgraphicsprogramming devshgraphicsprogramming commented Sep 16, 2025

Description

Continues #899 , #916 and #919

Testing

TODO list:

Comment on lines 377 to 383
Schlick<T> getReorientedFresnel(const scalar_type NdotI) NBL_CONST_MEMBER_FUNC
{
// correct? but also sclick works best between eta 1.4-2.2
OrientedEtaRcps<T> rcpEta = getOrientedEtaRcps();
T sqrt_newF0 = (hlsl::promote<T>(1.0) - rcpEta.value) / (hlsl::promote<T>(1.0) + rcpEta.value);
return Schlick<T>::create(sqrt_newF0 * sqrt_newF0);
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Schlick can't bea twosided fresnel, it only works for the front face

Comment on lines 315 to 331
((NBL_CONCEPT_REQ_EXPR_RET_TYPE)((fresnel.getOrientedEtaRcps()), ::nbl::hlsl::is_same_v, OrientedEtaRcps<typename T::vector_type>))
);
#undef cosTheta
#undef fresnel
#include <nbl/builtin/hlsl/concepts/__end.hlsl>

#define NBL_CONCEPT_NAME TwoSidedFresnel
#define NBL_CONCEPT_TPLT_PRM_KINDS (typename)
#define NBL_CONCEPT_TPLT_PRM_NAMES (T)
#define NBL_CONCEPT_PARAM_0 (fresnel, T)
#define NBL_CONCEPT_PARAM_1 (cosTheta, typename T::scalar_type)
NBL_CONCEPT_BEGIN(2)
#define fresnel NBL_CONCEPT_PARAM_T NBL_CONCEPT_PARAM_0
#define cosTheta NBL_CONCEPT_PARAM_T NBL_CONCEPT_PARAM_1
NBL_CONCEPT_END(
((NBL_CONCEPT_REQ_TYPE_ALIAS_CONCEPT)(Fresnel, T))
((NBL_CONCEPT_REQ_EXPR_RET_TYPE)((fresnel.getRefractionOrientedEta()), ::nbl::hlsl::is_same_v, OrientedEtas<typename T::vector_type>))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whats the difference between getOrientedEtaRcps and getRefractionOrientedEta ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, one is the reciprocal, btw getRefractionOrientedEta must be a scalar type return cause its used for geometric refraction

Comment on lines +34 to 48
namespace dummy_impl
{
using sample_t = SLightSample<ray_dir_info::SBasic<float> >;
using interaction_t = surface_interactions::SAnisotropic<surface_interactions::SIsotropic<ray_dir_info::SBasic<float> > >;
using cache_t = SAnisotropicMicrofacetCache<SIsotropicMicrofacetCache<float> >;
}

#define NBL_CONCEPT_NAME NDF
#define NBL_CONCEPT_TPLT_PRM_KINDS (typename)
#define NBL_CONCEPT_TPLT_PRM_NAMES (T)
#define NBL_CONCEPT_PARAM_0 (ndf, T)
#define NBL_CONCEPT_PARAM_1 (query, dummy_impl::MetaQuery)
#define NBL_CONCEPT_PARAM_1 (quant_query, typename T::quant_query_type)
#define NBL_CONCEPT_PARAM_2 (_sample, dummy_impl::sample_t)
#define NBL_CONCEPT_PARAM_3 (interaction, dummy_impl::interaction_t)
#define NBL_CONCEPT_PARAM_4 (cache, dummy_impl::cache_t)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't you take the float in the dummy_impl to be T::scalar_type or something from the NDF

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why but I'm unable to do

template<typename T>
using sample_t = SLightSample<ray_dir_info::SBasic<T> >;

and pass in T::scalar_type without the concept not working

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't you do typename T::scalar_type or something ?

Comment on lines +281 to +300
NBL_CONSTEXPR_STATIC_INLINE bool IsAnisotropic = _IsAnisotropic;
NBL_CONSTEXPR_STATIC_INLINE MicrofacetTransformTypes NDFSurfaceType = reflect_refract;
NBL_CONSTEXPR_STATIC_INLINE bool IsBSDF = reflect_refract != MTT_REFLECT;

using this_t = GGX<T, _IsAnisotropic, reflect_refract>;
using scalar_type = T;
using base_type = impl::GGXCommon<T,IsBSDF,IsAnisotropic>;
using quant_type = SDualMeasureQuant<scalar_type>;
using vector2_type = vector<T, 2>;
using vector3_type = vector<T, 3>;

using dg1_query_type = impl::SGGXDG1Query<scalar_type>;
using g2g1_query_type = impl::SGGXG2XQuery<scalar_type>;
using quant_query_type = impl::NDFQuantQuery<scalar_type>;

NBL_CONSTEXPR_STATIC_INLINE BxDFClampMode _clamp = IsBSDF ? BxDFClampMode::BCM_ABS : BxDFClampMode::BCM_NONE;
template<class Interaction>
NBL_CONSTEXPR_STATIC_INLINE bool RequiredInteraction = IsAnisotropic ? surface_interactions::Anisotropic<Interaction> : surface_interactions::Isotropic<Interaction>;
template<class MicrofacetCache>
NBL_CONSTEXPR_STATIC_INLINE bool RequiredMicrofacetCache = IsAnisotropic ? AnisotropicMicrofacetCache<MicrofacetCache> : ReadableIsotropicMicrofacetCache<MicrofacetCache>;
Copy link
Member Author

@devshgraphicsprogramming devshgraphicsprogramming Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks to me like every NDF needs to define:

this_t
scalar_type
vector2_type
vector3_type
IsAnisotropic
IsBSDF // this is badly named, should be SupportsTransmission or !ReflectanceOnly cause NDF is a concept apart and below a BSDF
NDFSurfaceType // should be renmaed to avoid the NDF::NDF tautology, maybe SupportedPaths
_Clamp
RequiredInteraction
RequiredMicrofacetCache

lets make it into a macro in ndf.hlsl

Comment on lines +130 to 135
static scalar_type G2_over_G1(NBL_CONST_REF_ARG(Query) query, NBL_CONST_REF_ARG(MicrofacetCache) cache)
{
scalar_type onePlusLambda_V = scalar_type(1.0) + query.getLambdaV();
return onePlusLambda_V * hlsl::mix(scalar_type(1.0)/(onePlusLambda_V + query.getLambdaL()), bxdf::beta<scalar_type>(onePlusLambda_V, scalar_type(1.0) + query.getLambdaL()), cache.isTransmission());
scalar_type lambda_L = query.getLambdaL();
return onePlusLambda_V * hlsl::mix(scalar_type(1.0)/(onePlusLambda_V + lambda_L), bxdf::beta<scalar_type>(onePlusLambda_V, scalar_type(1.0) + lambda_L), cache.isTransmission());
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this method seems identical for Anisotropic and Isotropic bechmann, why specialize twice?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually 3 or 4 methods (G2_over_G1, correlated, Lambda, DG1 Query overload) here seem identical with templated queries, its just the template constraint on the cache that changes, should be moved down into the most derived class

Comment on lines +326 to +339
template<class MicrofacetCache, typename C=bool_constant<!IsBSDF> NBL_FUNC_REQUIRES(RequiredMicrofacetCache<MicrofacetCache>)
enable_if_t<C::value && !IsBSDF, quant_query_type> createQuantQuery(NBL_CONST_REF_ARG(MicrofacetCache) cache, scalar_type orientedEta)
{
quant_query_type dummy; // brdfs don't make use of this
return dummy;
}
template<class MicrofacetCache, typename C=bool_constant<IsBSDF> NBL_FUNC_REQUIRES(RequiredMicrofacetCache<MicrofacetCache>)
enable_if_t<C::value && IsBSDF, quant_query_type> createQuantQuery(NBL_CONST_REF_ARG(MicrofacetCache) cache, scalar_type orientedEta)
{
quant_query_type quant_query;
quant_query.VdotHLdotH = cache.getVdotHLdotH();
quant_query.VdotH_etaLdotH = cache.getVdotH() + orientedEta * cache.getLdotH();
return quant_query;
}
Copy link
Member Author

@devshgraphicsprogramming devshgraphicsprogramming Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First, NBL_FUNC_REQUIRES is itself an enable_if, so I wouldn't write

template<class MicrofacetCache, typename C=bool_constant<!IsBSDF> NBL_FUNC_REQUIRES(RequiredMicrofacetCache<MicrofacetCache>)
enable_if_t<C::value && !IsBSDF, quant_query_type> createQuantQuery(NBL_CONST_REF_ARG(MicrofacetCache) cache, scalar_type orientedEta)

but simply

template<class MicrofacetCache, typename C=bool_constant<!IsBSDF> NBL_FUNC_REQUIRES(RequiredMicrofacetCache<MicrofacetCache> && C::value && !IsBSDF)
quant_query_type createQuantQuery(NBL_CONST_REF_ARG(MicrofacetCache) cache, scalar_type orientedEta)

Also in cases where the if-else branch wont generate invalid code, you can use NBL_IF_CONSTEXPR without worry

template<class MicrofacetCache> NBL_FUNC_REQUIRES(RequiredMicrofacetCache<MicrofacetCache>)
quant_query_type createQuantQuery(NBL_CONST_REF_ARG(MicrofacetCache) cache, scalar_type orientedEta)
{
   quant_query_type retval; // only has members for refraction
   NBL_IF_CONSTEXPR(SupportsTransmission)
   {
        retval.VdotHLdotH = cache.getVdotHLdotH();
        retval.VdotH_etaLdotH = cache.getVdotH() + orientedEta * cache.getLdotH();
   }
   return retval;
}

Finally, don't the getVdotH and getLdotH need ABS clamping or something?
If no, explain why in the comments

Copy link
Member Author

@devshgraphicsprogramming devshgraphicsprogramming Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is the NDFQuantQuery which seems only useful for ndf/microfacet_to_light_transform.hlsl not defined there? and outside of impl with a better name?

Comment on lines +378 to +402
template<class LS, class Interaction, class MicrofacetCache, typename C=bool_constant<!IsBSDF> NBL_FUNC_REQUIRES(LightSample<LS> && RequiredInteraction<Interaction> && RequiredMicrofacetCache<MicrofacetCache>)
enable_if_t<C::value && !IsBSDF, quant_type> D(NBL_CONST_REF_ARG(quant_query_type) quant_query, NBL_CONST_REF_ARG(LS) _sample, NBL_CONST_REF_ARG(Interaction) interaction, NBL_CONST_REF_ARG(MicrofacetCache) cache)
{
scalar_type d = __ndf_base.template D<MicrofacetCache>(cache);
return createDualMeasureQuantity<T>(d, interaction.getNdotV(BxDFClampMode::BCM_MAX), _sample.getNdotL(BxDFClampMode::BCM_MAX));
}
template<class LS, class Interaction, class MicrofacetCache, typename C=bool_constant<IsBSDF> NBL_FUNC_REQUIRES(LightSample<LS> && RequiredInteraction<Interaction> && RequiredMicrofacetCache<MicrofacetCache>)
enable_if_t<C::value && IsBSDF, quant_type> D(NBL_CONST_REF_ARG(quant_query_type) quant_query, NBL_CONST_REF_ARG(LS) _sample, NBL_CONST_REF_ARG(Interaction) interaction, NBL_CONST_REF_ARG(MicrofacetCache) cache)
{
scalar_type d = __ndf_base.template D<MicrofacetCache>(cache);
return createDualMeasureQuantity<T, reflect_refract>(d, interaction.getNdotV(BxDFClampMode::BCM_ABS), _sample.getNdotL(BxDFClampMode::BCM_ABS), quant_query.getVdotHLdotH(), quant_query.getVdotH_etaLdotH());
}

template<class LS, class Interaction, typename C=bool_constant<!IsBSDF> NBL_FUNC_REQUIRES(LightSample<LS> && RequiredInteraction<Interaction>)
enable_if_t<C::value && !IsBSDF, quant_type> DG1(NBL_CONST_REF_ARG(dg1_query_type) query, NBL_CONST_REF_ARG(quant_query_type) quant_query, NBL_CONST_REF_ARG(LS) _sample, NBL_CONST_REF_ARG(Interaction) interaction)
{
scalar_type dg1 = base_type::template DG1<dg1_query_type>(query);
return createDualMeasureQuantity<T>(dg1, interaction.getNdotV(BxDFClampMode::BCM_MAX), _sample.getNdotL(BxDFClampMode::BCM_MAX));
}
template<class LS, class Interaction, typename C=bool_constant<IsBSDF> NBL_FUNC_REQUIRES(LightSample<LS> && RequiredInteraction<Interaction>)
enable_if_t<C::value && IsBSDF, quant_type> DG1(NBL_CONST_REF_ARG(dg1_query_type) query, NBL_CONST_REF_ARG(quant_query_type) quant_query, NBL_CONST_REF_ARG(LS) _sample, NBL_CONST_REF_ARG(Interaction) interaction)
{
scalar_type dg1 = base_type::template DG1<dg1_query_type>(query);
return createDualMeasureQuantity<T, reflect_refract>(dg1, interaction.getNdotV(BxDFClampMode::BCM_ABS), _sample.getNdotL(BxDFClampMode::BCM_ABS), quant_query.getVdotHLdotH(), quant_query.getVdotH_etaLdotH());
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw you could have a createDualMeasureQuantity that takes the Quant Query instead of individual VdotHLdotH and VdotH_etaLDotH arguments, then you wouldn't need two copies each of D and DG1

Comment on lines +99 to 103
template<class Query NBL_FUNC_REQUIRES(ggx_concepts::DG1Query<Query>)
static scalar_type DG1(NBL_CONST_REF_ARG(Query) query)
{
return scalar_type(0.5) * query.getNdf() * query.getG1over2NdotV();
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is identical for isotropic and anisotropic, and so are the query taking methods: G1_wo_numerator_devsh_part, correlated_wo_numerator, correlated, G2_over_G1 as long as your requires clauses use RequiredInteraction and RequiredMicrofacetCache

Comment on lines 43 to 92
template<class N, class F, bool IsBSDF>
struct quant_query_helper;

template<class N, class F>
struct quant_query_helper<N, F, true>
{
using quant_query_type = typename N::quant_query_type;

template<class C>
static quant_query_type __call(NBL_REF_ARG(N) ndf, NBL_CONST_REF_ARG(F) fresnel, NBL_CONST_REF_ARG(C) cache)
{
return ndf.template createQuantQuery<C>(cache, fresnel.orientedEta.value[0]);
}
};

template<class N, class F>
struct quant_query_helper<N, F, false>
{
using quant_query_type = typename N::quant_query_type;

template<class C>
static quant_query_type __call(NBL_REF_ARG(N) ndf, NBL_CONST_REF_ARG(F) fresnel, NBL_CONST_REF_ARG(C) cache)
{
typename N::scalar_type dummy;
return ndf.template createQuantQuery<C>(cache, dummy);
}
};

template<class F, bool IsBSDF>
struct check_TIR_helper;

template<class F>
struct check_TIR_helper<F, false>
{
template<class MicrofacetCache>
static bool __call(NBL_CONST_REF_ARG(F) fresnel, NBL_CONST_REF_ARG(MicrofacetCache) cache)
{
return true;
}
};

template<class F>
struct check_TIR_helper<F, true>
{
template<class MicrofacetCache>
static bool __call(NBL_CONST_REF_ARG(F) fresnel, NBL_CONST_REF_ARG(MicrofacetCache) cache)
{
return cache.isValid(fresnel.getRefractionOrientedEta());
}
};
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think its possible to move these into NBL_IUF_CONSTEXPR if you just made a struct which just concerns itself with

scalar_type orientedEta = impl::get_orientedEta_helper(fresnel); // return `fresnel.getRefractionOrientedEta()` if `SupportsTransmission` requiring a twosided fresnel, otherwise return uninitialized var

Copy link
Member Author

@devshgraphicsprogramming devshgraphicsprogramming Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well your getOrientedFrensel already does what my suggested getOrientedEta_helper should do, (minus a call to getRefractionOrientedEta()

NBL_IF_CONSTEXPR(IsBSDF)
{
_f = impl::getOrientedFresnel<fresnel_type, IsBSDF>::__call(fresnel, interaction.getNdotV());
valid = impl::check_TIR_helper<fresnel_type, IsBSDF>::template __call<MicrofacetCache>(_f, cache);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you don't need check_TIR_helper, just call the cache.isValid() directly

Comment on lines +150 to +151
scalar_type dummy;
quant_query_type qq = ndf.template createQuantQuery<MicrofacetCache>(cache, dummy);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whats the dummy in place of?

Comment on lines +181 to +183
ray_dir_info_type invalidL;
invalidL.makeInvalid();
return sample_type::createFromTangentSpace(invalidL, interaction.getFromTangentSpace());
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's probably a faster way to make an invalid sample, require a createInvalid factory

template<typename C=bool_constant<!IsBSDF> >
enable_if_t<C::value && !IsBSDF, sample_type> generate(NBL_CONST_REF_ARG(anisotropic_interaction_type) interaction, const vector2_type u, NBL_REF_ARG(anisocache_type) cache)
{
if (interaction.getNdotV() > numeric_limits<scalar_type>::min)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

either rewrite the if so its > and put the valid case code inside, or assert that getNdotV is not NaN, as it stands a NaN will proceed

Comment on lines +213 to +219
L.makeInvalid(); // should check if sample direction is invalid

const vector3_type T = interaction.getT();
const vector3_type B = interaction.getB();
const vector3_type _N = interaction.getN();

return sample_type::create(L, T, B, _N);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there should really be just a sample_type::createInvalid and this vode with TBN should go into the valid if-case

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also the NdotL computation should be overwritten by 2.f*cache.getVdotH()*localH.z - localV.z which you compute for the if-statement (the quantitiy you check to see if the sample has invalid path).

ray_dir_info_type L;
if (scalar_type(2.0) * VdotH * localH.z > localV.z) // NdotL>0, compiler's Common Subexpression Elimination pass should re-use 2*VdotH later
{
assert(VdotH >= scalar_type(0.0));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can asset this as soon as VdotH is known with a comment about VNDF sampling

Comment on lines +267 to +269
// fail if samples have invalid paths
const scalar_type NdotL = scalar_type(2.0) * VdotH * localH.z - localV.z;
if ((ComputeMicrofacetNormal<scalar_type>::isTransmissionPath(NdotV, NdotL) != transmitted))
Copy link
Member Author

@devshgraphicsprogramming devshgraphicsprogramming Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hoist this check all the way to when VdotH and transmitted is first calculated

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be good to record all this in a comment #930 (comment)

const vector3_type upperHemisphereV = ieee754::flipSignIfRHSNegative<vector3_type>(localV, hlsl::promote<vector3_type>(NdotV));
const vector3_type localH = ndf.generateH(upperHemisphereV, u.xy);
const scalar_type VdotH = hlsl::dot(localV, localH);
const vector3_type H = hlsl::mul(interaction.getFromTangentSpace(), localH);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

push calculation till last moment

const vector3_type B = interaction.getB();

// fail if samples have invalid paths
const scalar_type NdotL = scalar_type(2.0) * VdotH * localH.z - localV.z;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not a valid computation of NdotL for the BSDF, this is the reflection equation

Copy link
Member Author

@devshgraphicsprogramming devshgraphicsprogramming Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you follow

vector_type operator()(const bool doRefract, const scalar_type rcpOrientedEta, NBL_REF_ARG(scalar_type) out_IdotTorR) NBL_CONST_MEMBER_FUNC
{
scalar_type NdotI = getNdotR();
const scalar_type a = hlsl::mix<scalar_type>(1.0f, rcpOrientedEta, doRefract);
const scalar_type b = NdotI * a + getNdotTorR(doRefract, rcpOrientedEta);
// assuming `I` is normalized
out_IdotTorR = NdotI * b - a;
return refract.N * b - refract.I * a;
}

then using the NdotH==localH.z, NdotV and the LdotH you already compute for cache.isValid() you can compute NdotL as:

const float viewShortenFactor = hlsl::mix<scalar_type>(1.0f, rcpOrientedEta, transmitted);
const float NdotL = localH.z * (VdotH*viewShortenFactor + LdotH) - NdotV * viewShortenFactor;

const vector3_type _N = interaction.getN();
Refract<scalar_type> r = Refract<scalar_type>::create(V.getDirection(), H);
const scalar_type LdotH = hlsl::mix(VdotH, r.getNdotT(rcpEta.value2[0]), transmitted);
cache = anisocache_type::createPartial(VdotH, LdotH, hlsl::dot(_N, H), transmitted, rcpEta);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you don't need to compute dot(_N,H) its literally localH.z

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also assert that the cache.isValid() right away after this (it should be, by construction)

Comment on lines +252 to +254
return rr(NdotTorR, rcpOrientedEta);
}
bxdf::ReflectRefract<scalar_type> rr;
Copy link
Member Author

@devshgraphicsprogramming devshgraphicsprogramming Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leave a comment that the call rr makes to getNdotTorR and mix as well as a good part of the comuptations should CSE with our computation of NdotL (which should be hoisted above the reflect/refract code)

Comment on lines 315 to 326
template<typename C=bool_constant<!IsAnisotropic> >
enable_if_t<C::value && !IsAnisotropic, scalar_type> pdf(NBL_CONST_REF_ARG(sample_type) _sample, NBL_CONST_REF_ARG(isotropic_interaction_type) interaction, NBL_CONST_REF_ARG(isocache_type) cache)
{
if (IsBSDF || (_sample.getNdotL() > numeric_limits<scalar_type>::min && interaction.getNdotV() > numeric_limits<scalar_type>::min))
{
scalar_type _pdf = __pdf<isotropic_interaction_type, isocache_type>(_sample, interaction, cache);
return hlsl::mix(scalar_type(0.0), _pdf, _pdf < bit_cast<scalar_type>(numeric_limits<scalar_type>::infinity));
}
else
return scalar_type(0.0);
}
scalar_type pdf(NBL_CONST_REF_ARG(sample_type) _sample, NBL_CONST_REF_ARG(anisotropic_interaction_type) interaction, NBL_CONST_REF_ARG(anisocache_type) cache)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can collapse the two methods if you use a REQUIRES(RequiredInteraction<Interaction> && RequiredCache<Cache>) similarly for other methods

Comment on lines +360 to +368
template<typename C=bool_constant<!IsAnisotropic> >
enable_if_t<C::value && !IsAnisotropic, quotient_pdf_type> quotient_and_pdf(NBL_CONST_REF_ARG(sample_type) _sample, NBL_CONST_REF_ARG(isotropic_interaction_type) interaction, NBL_CONST_REF_ARG(isocache_type) cache)
{
return __quotient_and_pdf<isotropic_interaction_type, isocache_type>(_sample, interaction, cache);
}
quotient_pdf_type quotient_and_pdf(NBL_CONST_REF_ARG(sample_type) _sample, NBL_CONST_REF_ARG(anisotropic_interaction_type) interaction, NBL_CONST_REF_ARG(anisocache_type) cache)
{
return __quotient_and_pdf<anisotropic_interaction_type, anisocache_type>(_sample, interaction, cache);
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could get rid of simple passthrough if you put a REQUIRES on the __quotient_and_pdf and removed the __

Comment on lines +153 to +160
quant_type D = ndf.template D<sample_type, Interaction, MicrofacetCache>(qq, _sample, interaction, cache);
scalar_type DG = D.projectedLightMeasure;
if (D.microfacetMeasure < bit_cast<scalar_type>(numeric_limits<scalar_type>::infinity))
{
using g2g1_query_type = typename ndf_type::g2g1_query_type;
g2g1_query_type gq = ndf.template createG2G1Query<sample_type, Interaction>(_sample, interaction);
DG *= ndf.template correlated<sample_type, Interaction>(gq, _sample, interaction);
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm actually since Eval is not Quotient, it should return 0 whenever PDF would have been INF, and therefore whenever Eval is inf, you can just return 0

you could even add an NBL_REF_ARG(bool) to the D signature for a boolean side-channel of isInf because it pays for the NDF to set that boolean as soon as possible (before a chain or multiplies and divides, compiler won't hoiust your INF checks before divisions by measure factors, etc.)

Comment on lines +153 to +159
quant_type D = ndf.template D<sample_type, Interaction, MicrofacetCache>(qq, _sample, interaction, cache);
scalar_type DG = D.projectedLightMeasure;
if (D.microfacetMeasure < bit_cast<scalar_type>(numeric_limits<scalar_type>::infinity))
{
using g2g1_query_type = typename ndf_type::g2g1_query_type;
g2g1_query_type gq = ndf.template createG2G1Query<sample_type, Interaction>(_sample, interaction);
DG *= ndf.template correlated<sample_type, Interaction>(gq, _sample, interaction);
Copy link
Member Author

@devshgraphicsprogramming devshgraphicsprogramming Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we must let NDF provide a Dcorrelated method, and if detected, use that instead of this, because we miss an optimization opportunity with GGX

We could actually do this in the following way

        using quant_query_type = typename ndf_type::quant_query_type;
        scalar_type dummy;
        quant_query_type qq = ndf.template createQuantQuery<MicrofacetCache>(cache, dummy);

        quant_type D = ndf.template D<sample_type, Interaction, MicrofacetCache>(qq, _sample, interaction, cache);
        scalar_type DG = D.projectedLightMeasure;
        if (D.microfacetMeasure < bit_cast<scalar_type>(numeric_limits<scalar_type>::infinity))
        {
            using g2g1_query_type = typename ndf_type::g2g1_query_type;
            g2g1_query_type gq = ndf.template createG2G1Query<sample_type, Interaction>(_sample, interaction);
            DG *= ndf.template correlated<sample_type, Interaction>(gq, _sample, interaction);
        }
        
        // overwrites DG with NDF's `Dcorrelated` method, also `asserts` that the optimal method returns similar to above
        impl::overwrite_DG<ndf_type,sample_type,Interaction,Cache>(/*NBL_REF_ARG*/DG,ndf,...);

continues #930 (comment)

using scalar_type = T;

scalar_type getNdf() NBL_CONST_MEMBER_FUNC { return ndf; }
scalar_type getG1over2NdotV() NBL_CONST_MEMBER_FUNC { return G1_over_2NdotV; }
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

want to either comment or stick it in the name or the Query concept comment that its the wo_numerator

Comment on lines +100 to 102
static scalar_type DG1(NBL_CONST_REF_ARG(Query) query)
{
return scalar_type(0.5) * query.getNdf() * query.getG1over2NdotV();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Query is not G1, this kinda seems to not make sense to me as a thing to return from DG1, I'm expecting NDF measure times the real G1 from a function called DG1

A rename might be in order, into DG1over4NdotV?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants